/*
 * Copyright 2010-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.newpath.core.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.code.newpath.config.BeanResourceConfig;
import com.google.code.newpath.config.ConfigException;
import com.google.code.newpath.core.Path;
import com.google.code.newpath.core.PathMatchPattern;
import com.google.code.newpath.core.Resource;
import com.google.code.newpath.core.ResourceCreator;
import com.google.code.newpath.core.ResourceConfig;
import com.google.code.newpath.core.ResourceFactory;
import com.google.code.newpath.core.exception.ResourceException;
import com.google.code.newpath.core.exception.ResourceNotFoundException;

/**
 * 
 * Definition of default root resources factory implementation.
 * This class is the gateway of all resource in one pathful container.
 * As general usage, Every resource should load from this root resource factory.
 * 
 *  1. register
 *  2. get resource
 *  3. cache
 *  4. flush
 *  
 *  
 *  FIXME need add configuration reading, configuration parsing, configuration processing.
 * 
 * @author Charlie Zhang
 *
 */
public class JsonConfigResourceFactory implements ResourceFactory<Resource<?>> {
	
	private Map<Path, Resource<?>> resourceCache = new HashMap<Path, Resource<?>>();
	
	private List<CreatorMatchPair> matchPairs = new ArrayList<CreatorMatchPair>();
	
	private Set<String> suffixSet = new HashSet<String>();
	
	private Map<Path, ResourceConfig> beanConfigMap = new LinkedHashMap<Path, ResourceConfig>();
	
	/**
	 * Create ResourceFactory from JSON format configuration. 
	 * Read JSON content from String parameter. 
	 * @param jsonContent
	 */
	public JsonConfigResourceFactory(String jsonContent) throws JsonParseException {
		
		JsonFactory jsonFactory = new JsonFactory(); // or, for data binding, org.codehaus.jackson.mapper.MappingJsonFactory 
		
		JsonParser jp;
		try {
			jp = jsonFactory.createJsonParser(jsonContent);
			ObjectMapper mapper = new ObjectMapper();
			JsonNode rootNode = mapper.readTree(jp);
			
			JsonNode beansNode = rootNode.get("beans");
			
			
			if(beansNode != null) {
				int beansSize = beansNode.size();
				for(int i=0 ; i<beansSize ; i++) {
					JsonNode beanNode = beansNode.get(i);
					parseBeanNode(beanNode);
				}
			
			}
			
		} 
		catch (Exception e) {
			throw new ConfigException("Failed to parse json content! \n" + jsonContent, e);
		} 

	}
	
	private void parseBeanNode(JsonNode beanNode) {
		BeanResourceConfig beanConfig = new BeanResourceConfig();
		
		beanConfig.setPath(new Path(beanNode.get("path").asText()));
		
		//If has no indicated property, it will return String("");
		//So the scope, class, initMethod, destroy
		beanConfig.setClassName(beanNode.path("class").asText());
		beanConfig.setScope(beanNode.path("scope").asText());
		beanConfig.setInitMethod(beanNode.path("initMethod").asText());
		beanConfig.setDestroyMethod(beanNode.path("destroyMethod").asText());
		
		//Set properties 
		JsonNode propertiesNode = beanNode.get("properties");
		if(propertiesNode != null && propertiesNode.size() > 0) {
			Map<String, Object> properties = new LinkedHashMap<String, Object>();
			
			//By this step, the resource target class type could not be recognized and also not get target property type, 
			//We just add the property as JsonNode into map value. 
			Iterator<Map.Entry<String, JsonNode>> it = propertiesNode.fields();
			while(it.hasNext()) {
				Map.Entry<String, JsonNode> property = it.next();
				properties.put(property.getKey(), property.getValue());
			}
			beanConfig.setProperties(properties);
		}

		if(beanConfigMap.containsKey(beanConfig.getPath())) {
			throw new ConfigException("Duplication defined the beans config for path:" + beanConfig.getPath());
		}
		
		beanConfigMap.put(beanConfig.getPath(), beanConfig);	
	}
	
	
	
	
	/**
	 * Create ResourceFactory from JSON format configuration. 
	 * Read JSON content from InputStream parameter. 
	 * @param jsonContent
	 */
	public JsonConfigResourceFactory(InputStream jsonIn) {
		
		
	}
	
	/**
	 * Create ResourceFactory from JSON format configuration. 
	 * Read JSON content from Reader parameter. 
	 * @param jsonContent
	 */
	public JsonConfigResourceFactory(Reader jsonReader) {
		
		
	}
	
	
	
	/**
	 * 
	 * @param pathPattern ant
	 * @param subFactory
	 */
	public void registerCreator(PathMatchPattern filterMathPattern, ResourceCreator creator) {
		matchPairs.add(new CreatorMatchPair(filterMathPattern, creator));
		
		suffixSet.add(creator.getSuffix());
	}
	

	public void registerCreator(String filterPathPattern, ResourceCreator creator) {
		registerCreator(new PathMatchPattern(new String[]{filterPathPattern}, null), creator);
	}
	

	public void registerCreator(ResourceCreator creator) {
		String[] includePathPatterns = {"/**/*." + creator.getSuffix()};
		registerCreator(new PathMatchPattern(includePathPatterns, null), creator);
	}
	
	
	public synchronized Resource<?> getResource(Path resourcePath) {
		Resource<?> resource = resourceCache.get(resourcePath);
		if(resource == null) {
			resource = fetchResource(resourcePath);
			if(resource != null) {
				resourceCache.put(resourcePath, resource);
			}
			else {
				throw new ResourceNotFoundException("Failed to find resource path=" + resourcePath, resourcePath);
			}
		}
		
		return resource;
	}
	
	
	public Resource<?> getResource(String fullPath) throws ResourceException {
		return getResource(new Path(fullPath));
	}

	
	/**
	 * Actually get resource from sub ResourceFactory.
	 */
	private Resource<?> fetchResource(Path fullPath) {
        for(CreatorMatchPair matchPair : matchPairs) {
        	if(matchPair.pathPattern.isMatch(fullPath)) {
        		
        		
        		//If found the ResourceConfig for this path, create the new Resource by ResourceConfig.
        		ResourceConfig resourceConfig = beanConfigMap.get(fullPath);
        		if(resourceConfig != null) {
	        		if(resourceConfig.isCreatable()) {
	        			return matchPair.creator.createResource(resourceConfig);
	        		}
	        		else {
	        			Resource<?> resource = matchPair.creator.createResource(fullPath);
		        		if(resource != null) {
		        			return resource;
		        		}
		        		//FIXME Add more operation for none creatable ResourceConfig
		        		throw new UnsupportedOperationException();
	        		}
        		}
        		else {
        			
	        		Resource<?> resource = matchPair.creator.createResource(fullPath);
	        		if(resource != null) {
	        			return resource;
	        		}
        		}
        	}
        }
        return null;
	}
	
	private class CreatorMatchPair {
		
		private final PathMatchPattern pathPattern;
		
		private final ResourceCreator<Resource<?>> creator;
		
		public CreatorMatchPair(PathMatchPattern pathPattern, ResourceCreator<Resource<?>> creator) {
			this.pathPattern = pathPattern;
			this.creator = creator;
		}

		public PathMatchPattern getPathPattern() {
			return pathPattern;
		}

		public ResourceCreator<Resource<?>> getCreator() {
			return creator;
		}
	}

	/**
	 * 
	 * Flush the all match resource which cached in current ResourceFactory
	 * 
	 * @param mathPattern  match pattern definition. The all resources which path match this pattern will be removed.
	 * 
	 */
	public synchronized void flush(PathMatchPattern mathPattern) {
		//Flush it's own cache for Reourses
		//To avoid the ConcurrentModificationException, we should do removing operations by following two step:
		//1 Find the match keys to be removed;
		Set<Path> keys = resourceCache.keySet();
		List<Path> removingKeys = new ArrayList<Path>();
		for(Path key : keys) {
			if(mathPattern.isMatch(key)) {
				removingKeys.add(key);
			}
		}
		
		//2.Actually remove the values from targetCache
		if(removingKeys.size() > 0) {
			for(Path removingKey : removingKeys) {
				resourceCache.remove(removingKey);
			}
		}
		
	}


	public String[] getSuffixes() {
		return suffixSet.toArray(new String[0]);
	}
	
	
	

}
